<?php
namespace app\common\service;

use think\facade\Db;
use ZipArchive;
use RuntimeException;

class MysqlBackup
{
    // 备份目录（runtime/backup/）
    private string $backupDir;
    // MySQL配置（仅用于读取信息，不传入connect方法）
    private array $dbConfig;
    // 忽略的表（可自定义）
    private array $ignoreTables = [
        'jkyl_areas',
        'jkyl_auth_rule',
        'jkyl_auth_rule_type',
        'jkyl_job_fail_log',
        'jkyl_tag',
        'jkyl_user_archive_option',
        'jkyl_user_archive_template',
        'jkyl_user_archive_type',
        'jkyl_platform_auth_rule',
        'jkyl_company'
    ];
    // 分卷大小（默认100M，单位：字节）
    private int $partSize = 104857600;
    // 是否压缩（Zip）
    private bool $compress = false;
    // 当前分卷号
    private int $part = 1;
    // 单次查询数据量（避免内存溢出）
    private int $pageSize = 2000;

    public function __construct()
    {
        // 初始化备份目录
        $this->backupDir = public_path() . 'backup' . DIRECTORY_SEPARATOR;
        $this->createDir($this->backupDir);

        // 获取MySQL默认配置（仅读取，不传入connect方法）
        $config = Db::getConfig();
        $this->dbConfig = $config['connections'][$config['default']];
    }

    /**
     * 设置分卷大小（单位：M）
     */
    public function setPartSize(int $sizeM): self
    {
        $this->partSize = $sizeM * 1024 * 1024;
        return $this;
    }

    /**
     * 设置忽略的表
     */
    public function setIgnoreTables(array $tables): self
    {
        $this->ignoreTables = array_merge($this->ignoreTables, $tables);
        return $this;
    }

    /**
     * 关闭压缩（导出纯SQL文件）
     */
    public function disableCompress(): self
    {
        $this->compress = false;
        return $this;
    }

    /**
     * 执行MySQL备份
     * @return array 备份结果
     * @throws RuntimeException
     */
    public function backup(): array
    {
        // 备份文件名（时间戳_数据库名）
        $backupName = date('YmdHis') . '_' . $this->dbConfig['database'];
        $basePath = $this->backupDir . $backupName;
        $fileExt = $this->compress ? '.zip' : '.sql';

        try {
            // 1. 获取所有需要备份的表
            $tables = $this->getAllTables();
            if (empty($tables)) {
                throw new RuntimeException('没有可备份的表（已过滤系统表）');
            }

            // 2. 初始化文件句柄（分卷/压缩）
            $handle = $this->initFileHandle($basePath);
            if (!$handle) {
                throw new RuntimeException('创建备份文件失败，请检查目录权限');
            }

            // 3. 写入备份头部信息（MySQL特有配置）
            $this->writeHeader($handle);

            // 4. 循环备份表结构+数据
            foreach ($tables as $table) {
                // 备份表结构
                $this->backupTableStructure($handle, $table);
                // 备份表数据
                $this->backupTableData($handle, $table);
            }

            // 5. 关闭文件句柄（压缩文件需finish）
            $this->closeFileHandle($handle, $basePath);

            return [
                'status' => true,
                'msg' => '备份成功',
                'data' => [
                    'filename' => $backupName . $fileExt,
                    'path' => './backup/'. $backupName . $fileExt,
                    'size' => filesize($basePath . $fileExt),
                    'part_count' => $this->part - 1
                ]
            ];
        } catch (RuntimeException $e) {
            // 备份失败，清理临时文件
            $this->cleanTempFiles($basePath);
            return ['status' => false, 'msg' => $e->getMessage()];
        }
    }

    /**
     * 恢复MySQL备份（优化大文件处理）
     * @param string $filePath 备份文件路径（.sql 或 .zip）
     * @return array 恢复结果
     * @throws RuntimeException
     */
    public function restore(string $filePath): array
    {
        if (!file_exists($filePath)) {
            throw new RuntimeException('备份文件不存在');
        }

        set_time_limit(0);
        ini_set('memory_limit', '-1');

        $isTemp = false;
        if (pathinfo($filePath, PATHINFO_EXTENSION) === 'zip') {
            $filePath = $this->unzipBackup($filePath);
            $isTemp = true;
        }

        $handle = fopen($filePath, 'r');
        if (!$handle) {
            throw new RuntimeException('无法打开备份文件');
        }

        try {
            // 仅保留不需要特殊权限的设置（移除SQL_LOG_BIN相关）
            Db::execute('SET FOREIGN_KEY_CHECKS = 0;'); // 关闭外键检查（普通权限可执行）
            Db::execute('SET UNIQUE_CHECKS = 0;'); // 关闭唯一键检查（普通权限可执行）
            Db::execute('SET autocommit = 0;'); // 关闭自动提交（普通权限可执行）
            Db::startTrans();

            // 流式执行SQL（后续优化批量执行逻辑）
            $this->executeSqlStream($handle);

            Db::commit();
            // 还原设置
            Db::execute('SET UNIQUE_CHECKS = 1;');
            Db::execute('SET autocommit = 1;');
            Db::execute('SET FOREIGN_KEY_CHECKS = 1;');

            return ['status' => true, 'msg' => '恢复成功'];
        } catch (\Exception $e) {
            Db::rollback();
            // 异常时还原设置
            Db::execute('SET UNIQUE_CHECKS = 1;');
            Db::execute('SET autocommit = 1;');
            Db::execute('SET FOREIGN_KEY_CHECKS = 1;');
            throw new RuntimeException('恢复失败：' . $e->getMessage());
        } finally {
            fclose($handle);
            if ($isTemp && file_exists($filePath)) {
                unlink($filePath);
            }
        }
    }

    /**
     * 流式执行SQL（分块读取并处理）
     * @param resource $handle 文件句柄
     */
    private function executeSqlStream($handle): void
    {
        $buffer = '';          // 缓冲区：积累SQL片段
        $blockSize = 1024*1024;     // 增大块大小，提升读取效率
        $inString = false;     // 是否处于字符串中（单引号内）
        $inComment = false;    // 是否处于单行注释中（-- 开头）
        $inMultiComment = false; // 是否处于多行注释中（/* */）
        $lastChar = '';        // 上一个字符（用于处理转义/注释）

        while (!feof($handle)) {
            $block = fread($handle, $blockSize);
            if ($block === false) {
                throw new RuntimeException('读取文件失败');
            }

            $length = strlen($block);
            for ($i = 0; $i < $length; $i++) {
                $char = $block[$i];
                $current = $lastChar . $char; // 组合上一个字符，用于识别注释

                // 处理多行注释 /* ... */
                if ($inMultiComment) {
                    if ($current === '*/') {
                        $inMultiComment = false;
                        $lastChar = ''; // 重置上一个字符，避免影响后续判断
                        continue; // 跳过注释结束符
                    }
                    continue; // 忽略注释内容
                }

                // 处理单行注释 -- ...（遇到换行结束）
                if ($inComment) {
                    if ($char === "\n" || $char === "\r") {
                        $inComment = false;
                    }
                    continue; // 忽略注释内容
                }

                // 处理单引号字符串（忽略字符串内的分号）
                if ($char === "'" && $lastChar !== '\\') {
                    $inString = !$inString;
                }

                // 识别单行注释 --
                if ($current === '--') {
                    $inComment = true;
                    $buffer = rtrim($buffer, '-'); // 移除缓冲区中已添加的 '-'
                    $lastChar = '';
                    continue;
                }

                // 识别多行注释 /*
                if ($current === '/*') {
                    $inMultiComment = true;
                    $buffer = rtrim($buffer, '/'); // 移除缓冲区中已添加的 '/'
                    $lastChar = '';
                    continue;
                }

                // 积累有效字符到缓冲区（非注释、非特殊情况）
                if (!$inComment && !$inMultiComment) {
                    $buffer .= $char;
                }

                // 当遇到分号且不在字符串中时，认为是一个完整的SQL语句
                if ($char === ';' && !$inString && !$inComment && !$inMultiComment) {
                    $sql = trim($buffer);
                    if (!empty($sql)) {
                        // 执行单条SQL（确保此时只有一条语句）
                        Db::execute($sql);
                    }
                    // 清空缓冲区，准备下一个语句
                    $buffer = '';
                }

                // 更新上一个字符（忽略空字符，避免影响注释判断）
                $lastChar = ($char === "\r" || $char === "\n") ? '' : $char;
            }
        }

        // 处理最后剩余的SQL片段（确保没有分号也能执行，如最后一条语句可能遗漏分号）
        $sql = trim($buffer);
        if (!empty($sql)) {
            Db::execute($sql);
        }
    }

// 新增批量执行方法
    private function executeBatchSql(array $sqls): void
    {
        if (empty($sqls)) {
            return;
        }
        // 拼接为单条SQL（分号分隔）
        $sql = implode(' ', $sqls);
        Db::execute($sql);
    }

    /**
     * 获取备份文件列表
     */
    public function getBackupList(): array
    {
        $list = [];
        if (!is_dir($this->backupDir)) {
            return $list;
        }

        $files = scandir($this->backupDir);
        foreach ($files as $file) {
            if ($file === '.' || $file === '..') {
                continue;
            }

            $filePath = $this->backupDir . $file;
            $ext = pathinfo($file, PATHINFO_EXTENSION);
            if (!in_array($ext, ['sql', 'zip'])) {
                continue;
            }

            $list[] = [
                'name' => $file,
                'path' => $filePath,
                'size' => filesize($filePath),
                'size_text' => $this->formatSize(filesize($filePath)),
                'time' => filemtime($filePath),
                'time_text' => date('Y-m-d H:i:s', filemtime($filePath)),
                'type' => $ext
            ];
        }

        // 按时间倒序排序
        usort($list, fn($a, $b) => $b['time'] - $a['time']);
        return $list;
    }

    /**
     * 删除备份文件
     */
    public function deleteBackup(string $filename): bool
    {
        $filePath = $this->backupDir . $filename;
        if (file_exists($filePath)) {
            return unlink($filePath);
        }
        return false;
    }

    // -------------------------- 私有工具方法 --------------------------

    /**
     * 获取所有需要备份的MySQL表
     */
    private function getAllTables(): array
    {
        // 直接使用默认连接，无需传入配置数组
        $tables = Db::query("SHOW TABLES FROM `{$this->dbConfig['database']}`");

        $result = [];
        foreach ($tables as $table) {
            $tableName = current($table);
            // 过滤忽略的表
            if (!in_array($tableName, $this->ignoreTables)) {
                $result[] = $tableName;
            }
        }
        return $result;
    }

    /**
     * 初始化文件句柄（支持压缩/分卷）
     */
    private function initFileHandle(string $basePath)
    {
        $filePath = $this->getPartFilePath($basePath);

        if ($this->compress) {
            $zip = new ZipArchive();
            if ($zip->open($filePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
                return false;
            }
            return $zip;
        } else {
            return fopen($filePath, 'w');
        }
    }

    /**
     * 获取分卷文件路径
     */
    private function getPartFilePath(string $basePath): string
    {
        $ext = $this->compress ? '.zip' : '.sql';
        return $this->part > 1 ? "{$basePath}_part{$this->part}{$ext}" : "{$basePath}{$ext}";
    }

    /**
     * 写入备份头部信息
     */
    private function writeHeader($handle): void
    {
        // 获取MySQL版本
        $mysqlVersion = Db::query('SELECT VERSION()')[0]['VERSION()'];

        $header = "-- ==============================\n";
        $header .= "-- MySQL数据库备份文件\n";
        $header .= "-- 备份时间：" . date('Y-m-d H:i:s') . "\n";
        $header .= "-- 数据库：" . $this->dbConfig['database'] . "\n";
        $header .= "-- 字符集：" . $this->dbConfig['charset'] . "\n";
        $header .= "-- MySQL版本：" . $mysqlVersion . "\n";
        $header .= "-- ==============================\n";
        $header .= "SET NAMES '{$this->dbConfig['charset']}';\n";
        $header .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";

        $this->writeContent($handle, $header);
    }

    /**
     * 备份MySQL表结构
     */
    private function backupTableStructure($handle, string $table): void
    {
        // 直接使用默认连接查询表结构
        $createSql = Db::query("SHOW CREATE TABLE `{$table}`")[0]['Create Table'];

        $content = "-- 备份表结构：{$table}\n";
        $content .= "DROP TABLE IF EXISTS `{$table}`;\n";
        $content .= "{$createSql};\n\n";

        $this->writeContent($handle, $content);
    }

    /**
     * 备份MySQL表数据（分批查询，避免内存溢出）
     */
    private function backupTableData($handle, string $table): void
    {
        // 直接使用默认连接查询数据
        $total = Db::table($table)->count();
        if ($total === 0) {
            return;
        }

        $content = "-- 备份表数据：{$table}（共{$total}条）\n";
        $this->writeContent($handle, $content);

        // 分批查询数据
        $pages = ceil($total / $this->pageSize);
        for ($page = 1; $page <= $pages; $page++) {
            $data = Db::table($table)
                ->page($page, $this->pageSize)
                ->select()
                ->toArray();

            if (empty($data)) {
                continue;
            }

            // 生成MySQL批量插入SQL
            $insertSql = $this->buildBatchInsertSql($table, $data);
            $this->writeContent($handle, $insertSql . "\n");
        }

        $this->writeContent($handle, "\n");
    }

    /**
     * 构建MySQL批量插入SQL
     */
    private function buildBatchInsertSql(string $table, array $data): string
    {
        $fields = array_keys(current($data));
        $fieldsStr = '`' . implode('`, `', $fields) . '`';

        $values = [];
        foreach ($data as $row) {
            $rowValues = [];
            foreach ($row as $value) {
                // MySQL字符串转义：直接使用默认连接的PDO实例
                if (is_null($value)) {
                    $rowValues[] = 'NULL';
                } elseif (is_bool($value)) {
                    $rowValues[] = $value ? 1 : 0;
                } elseif (is_string($value)) {
                    // 适配MySQL转义规则（使用默认连接的PDO）
                    $escaped = Db::getPDO()->quote($value);
                    $rowValues[] = $escaped;
                } else {
                    $rowValues[] = $value;
                }
            }
            $values[] = '(' . implode(', ', $rowValues) . ')';
        }

        return "INSERT INTO `{$table}` ({$fieldsStr}) VALUES " . implode(', ', $values) . ';';
    }

    /**
     * 写入内容到文件（支持Zip/普通文件）
     */
    private function writeContent($handle, string $content): void
    {
        if ($this->compress) {
            // Zip模式：写入到压缩包内的sql文件
            $zip = $handle;
            $innerFileName = $this->part > 1 ? "part{$this->part}.sql" : "backup.sql";

            // 检查当前分卷大小，超过则新建分卷
            if ($zip->filesize($innerFileName) + strlen($content) > $this->partSize) {
                $zip->close();
                $this->part++;
                $zip = $this->initFileHandle(dirname($this->getPartFilePath('')));
            }

            if ($zip->locateName($innerFileName) === false) {
                $zip->addFromString($innerFileName, $content);
            } else {
                $existing = $zip->getFromName($innerFileName);
                $zip->deleteName($innerFileName);
                $zip->addFromString($innerFileName, $existing . $content);
            }
        } else {
            // 普通文件模式：直接写入
            fwrite($handle, $content);
            // 检查分卷大小
            if (ftell($handle) > $this->partSize) {
                fclose($handle);
                $this->part++;
                $handle = $this->initFileHandle(dirname($this->getPartFilePath('')));
            }
        }
    }

    /**
     * 关闭文件句柄
     */
    private function closeFileHandle($handle, string $basePath): void
    {
        if ($this->compress) {
            $handle->close();
        } else {
            fclose($handle);
        }

        // 分卷备份时，创建分卷清单
        if ($this->part > 1) {
            $manifest = json_encode([
                'name' => basename($basePath),
                'part_count' => $this->part - 1,
                'create_time' => time(),
                'db' => $this->dbConfig['database']
            ], JSON_UNESCAPED_UNICODE);
            file_put_contents("{$basePath}_manifest.json", $manifest);
        }
    }

    /**
     * 解压备份文件
     */
    private function unzipBackup(string $zipPath): string
    {
        $unzipDir = $this->backupDir . 'unzip_' . uniqid() . DIRECTORY_SEPARATOR;
        $this->createDir($unzipDir);

        $zip = new ZipArchive();
        if ($zip->open($zipPath) !== true) {
            throw new RuntimeException('压缩文件损坏，无法解压');
        }

        $zip->extractTo($unzipDir);
        $zip->close();

        // 查找解压后的SQL文件
        $sqlFiles = glob($unzipDir . '*.sql');
        if (empty($sqlFiles)) {
            $this->deleteDir($unzipDir);
            throw new RuntimeException('解压后未找到SQL文件');
        }

        return $sqlFiles[0];
    }

    /**
     * 执行SQL语句（优化拆分逻辑，处理分号问题）
     */
    private function executeSql(string $sqlContent): void
    {
        // 移除注释（简化处理，避免注释中的分号干扰）
        $sqlContent = preg_replace('/--.*/', '', $sqlContent);
        // 按分号+换行拆分，但忽略字符串中的分号（简单处理单引号字符串）
        preg_match_all('/(?:[^;\']| \'[^\']*\' )+/', $sqlContent, $matches);

        foreach ($matches[0] as $sql) {
            $sql = trim($sql);
            if (empty($sql)) {
                continue;
            }
            // 补充分号（因为拆分时去掉了）
            $sql .= ';';
            Db::execute($sql);
        }
    }

    /**
     * 格式化文件大小
     */
    private function formatSize(int $size): string
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        for ($i = 0; $size >= 1024 && $i < 3; $i++) {
            $size /= 1024;
        }
        return round($size, 2) . ' ' . $units[$i];
    }

    /**
     * 创建目录
     */
    private function createDir(string $dir): void
    {
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
    }

    /**
     * 删除目录及文件
     */
    private function deleteDir(string $dir): void
    {
        if (is_dir($dir)) {
            $files = scandir($dir);
            foreach ($files as $file) {
                if ($file !== '.' && $file !== '..') {
                    $path = $dir . $file;
                    is_dir($path) ? $this->deleteDir($path) : unlink($path);
                }
            }
            rmdir($dir);
        }
    }

    /**
     * 清理临时文件
     */
    private function cleanTempFiles(string $basePath): void
    {
        $files = glob("{$basePath}*");
        foreach ($files as $file) {
            if (file_exists($file)) {
                unlink($file);
            }
        }
    }
}